首页 / 技术类 / C++ / 学习下 WTL 的 thunk

学习下 WTL 的 thunk

2010-10-24 16:44:00

由于 C++ 成员函数的调用机制问题,对C语言回调函数的 C++ 封装是件比较棘手的事。为了保持C++对象的独立性,理想情况是将回调函数设置到成员函数,而一般的回调函数格式通常是普通的C函数,尤其是 Windows API 中的。好在有些回调函数中留出了一个额外参数,这样便可以由这个通道将 this 指针传入。比如线程函数的定义为:

1typedef DWORD (WINAPI *PTHREAD_START_ROUTINE)(
2    LPVOID lpThreadParameter
3    );
4typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;

这样,当我们实现线程类的时候,就可以:

 1class Thread
 2{
 3private:
 4    HANDLE m_hThread;
 5
 6public:
 7    BOOL Create()
 8    {
 9        m_hThread = CreateThread(NULL, 0, StaticThreadProc, (LPVOID)this, 0, NULL);
10        return m_hThread != NULL;
11    }
12
13private:
14    DWORD WINAPI ThreadProc()
15    {
16        // TODO
17        return 0;
18    }
19
20private:
21    static DWORD WINAPI StaticThreadProc(LPVOID lpThreadParameter)
22    {
23        ((Thread *)lpThreadParameter)->ThreadProc();
24    }
25};

不过,这样,成员函数 ThreadProc()``` 便丧失了一个参数,这通常无伤大雅,任何原本需要从参数传入的信息都可以作为成员变量让 ```ThreadProc``` 来读写。如果一定有些什么是非从参数传入不可的,那也可以,一种做法,创建线程的时候传入一个包含 ```this 指针信息的结构。第二种做法,对该 class 作单例限制——如果现实情况允许的话。

所以,有额外参数的回调函数都好处理。不幸的是,Windows 的窗口回调函数没有这样一个额外参数:

1typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

这使得对窗口的 C++ 封装变得困难。为了解决这个问题,一个很自然的想法是,维护一份全局的窗口句柄到窗口类的对应关系,如:

 1#include <map>
 2
 3class Window
 4{
 5public:
 6    Window();
 7    ~Window();
 8    
 9public:
10    BOOL Create();
11
12protected:
13    LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
14
15protected:
16    HWND m_hWnd;
17
18protected:
19    static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
20    static std::map<HWND, Window *> m_sWindows;
21};
22
23 Create 的时候,指定 StaticWndProc 为窗口回调函数,并将 hWnd  this 存入 m_sWindows
24
25BOOL Window::Create()
26{
27    LPCTSTR lpszClassName = _T("ClassName");
28    HINSTANCE hInstance = GetModuleHandle(NULL);
29
30    WNDCLASSEX wcex    = { sizeof(WNDCLASSEX) };
31    wcex.lpfnWndProc   = StaticWndProc;
32    wcex.hInstance     = hInstance;
33    wcex.lpszClassName = lpszClassName;
34
35    RegisterClassEx(&wcex);
36
37    m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,
38        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
39
40    if (m_hWnd == NULL)
41    {
42        return FALSE;
43    }
44
45    m_sWindows.insert(std::make_pair(m_hWnd, this));
46
47    ShowWindow(m_hWnd, SW_SHOW);
48    UpdateWindow(m_hWnd);
49
50    return TRUE;
51}

StaticWindowProc``` 中,由 ```hWnd``` 找到 ```this,然后转发给成员函数:

1LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
2{
3    std::map<HWND, Window *>::iterator it = m_sWindows.find(hWnd);
4    assert(it != m_sWindows.end() && it->second != NULL);
5
6    return it->second->WndProc(message, wParam, lParam);
7}

m_sWindows 的多线程保护略过,下同)

据说 MFC 采用的就是类似的做法。缺点是,每次 StaticWndProc``` 都要从 ```m_sWindows``` 中去找 ```this```。由于窗口类一般会保存窗口句柄,回调函数里的 ```hWnd``` 就没多大作用了,如果这个 ```hWnd``` 能够被用来存 ```this 指针就好了,那么就能写成这样:

1LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
2{
3    return ((Window *)hWnd)->WndProc(message, wParam, lParam);
4}

这样看上去就爽多了。传说中 WTL 所采取的 thunk 技术就是这么干的。之前,只是听过这遥远的传说,今天,终于有机会走进这个传说去看一看。参考资料是一篇不知原始出处的文章《深入剖析WTL—WTL框架窗口分析》,以及部分 WTL 8.0 代码,还有其他乱七八糟的文章。

WTL 的思路是,每次在系统调用 WndProc 的时候,让它鬼使神差地先走到我们的另一处代码,让我们有机会修改堆栈中的 hWnd。这处代码可能是类似这样的:

1__asm
2{
3    mov dword ptr [esp+4], pThis  ;调用 WndProc 时,堆栈结构为:RetAddr, hWnd, message, wParam, lParam, ... 故 [esp+4]
4    jmp WndProc
5}

由于 pThis``` 和 ```WndProc 需要被事先修改(但又无法在编译前定好),所以我们需要运行的时候去修改这部分代码。先弄一个小程序探测下这两行语句的机器码:

 1LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
 2{
 3    return 0;
 4}
 5
 6int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
 7{
 8    MessageBox(NULL, NULL, NULL, MB_OK);
 9
10    __asm
11    {
12        mov dword ptr [esp+4], 1
13        jmp WndProc
14    }
15
16    return 0;
17}

最前面的 MessageBox 是为了等下调试的时候容易找到进入点。

然后使用 OllyDbg,在 MessageBoxW 上设置断点,执行到该函数返回:

这里我们看到,mov dword ptr [esp+4]``` 的机器码为 ```C7 44 24 04```,后面紧接着的一个 ```DWORD``` 是 ```mov``` 的第二个操作数。```jmp``` 的机器码是 ```e9```,后面紧接着的一个 ```DWORD 是跳转的相对地址。其中 00061000h - 0006102Bh = FFFFFFD5h。

于是定义这样一个结构:

1#pragma pack(push,1)
2typedef struct _StdCallThunk
3{
4    DWORD   m_mov;          // = 0x042444C7
5    DWORD   m_this;         // = this
6    BYTE    m_jmp;          // = 0xe9
7    DWORD   m_relproc;      // = relative distance
8} StdCallThunk;
9#pragma pack(pop)

这个结构可以作为窗口类的成员变量存在。我们的窗口类现在变成了这样子:

 1class Window
 2{
 3public:
 4    Window();
 5    ~Window();
 6
 7public:
 8    BOOL Create();
 9
10protected:
11    LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
12
13protected:
14    HWND         m_hWnd;
15    StdCallThunk m_thunk;
16
17protected:
18    static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
19};

似乎少了点什么……创建窗口的时候,我们是不能直接把回调函数设到 StaticWndPorc 中去的,因为这个函数是希望被写成这样子的:

1LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
2{
3    return ((Window *)hWnd)->WndProc(message, wParam, lParam);
4}

那么至少需要一个临时的回调函数,在这个函数里去设置新的回调函数(设到 m_thunk``` 上),再由 ```m_thunk``` 来调用 ```StaticWndProc```,```StaticWndProc``` 再去调用 ```WndProc,这样整个过程就通了。

但是,临时回调函数还是需要知道从 hWnd``` 到 ```this``` 的对应关系。可是现在我们不能照搬用刚才的 ```m_sWindows``` 了。因为窗口在创建过程中就会调用到回调函数,需要使用到 ```m_sWindows``` 里的 ```this```,而窗口被成功创建之前,我们没法提前拿到 ```HWND``` 存入 ```m_sWindows```。现在,换个方法,存当前线程 ID 与 ```this 的对应关系。这样,这个类变成了:

 1#include <map>
 2
 3class Window
 4{
 5public:
 6    Window();
 7    ~Window();
 8
 9public:
10    BOOL Create();
11
12protected:
13    LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
14
15protected:
16    HWND         m_hWnd;
17    StdCallThunk m_thunk;
18
19protected:
20    static LRESULT CALLBACK TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
21
22    static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
23    static std::map<DWORD, Window *> m_sWindows;
24};
25然后实现 Create  TempWndProc
26
27BOOL Window::Create()
28{
29    LPCTSTR lpszClassName = _T("ClassName");
30    HINSTANCE hInstance = GetModuleHandle(NULL);
31
32    WNDCLASSEX wcex    = { sizeof(WNDCLASSEX) };
33    wcex.lpfnWndProc   = TempWndProc;
34    wcex.hInstance     = hInstance;
35    wcex.lpszClassName = lpszClassName;
36
37    RegisterClassEx(&wcex);
38
39    DWORD dwThreadId = GetCurrentThreadId();
40    m_sWindows.insert(std::make_pair(dwThreadId, this));
41
42    m_thunk.m_mov = 0x042444c7;
43    m_thunk.m_jmp = 0xe9;
44
45    m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,
46        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
47
48    if (m_hWnd == NULL)
49    {
50        return FALSE;
51    }
52    
53    ShowWindow(m_hWnd, SW_SHOW);
54    UpdateWindow(m_hWnd);
55
56    return TRUE;
57}
58
59LRESULT CALLBACK Window::TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
60{
61    std::map<DWORD, Window *>::iterator it = m_sWindows.find(GetCurrentThreadId());
62    assert(it != m_sWindows.end() && it->second != NULL);
63
64    Window *pThis = it->second;
65    m_sWindows.erase(it);
66
67    WNDPROC pWndProc = (WNDPROC)&pThis->m_thunk;
68
69    pThis->m_thunk.m_this = (DWORD)pThis;
70    pThis->m_thunk.m_relproc = (DWORD)&Window::StaticWndProc - ((DWORD)&pThis->m_thunk + sizeof(StdCallThunk));
71
72    m_hWnd = hWnd;
73    SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pWndProc);
74
75    return pWndProc(hWnd, message, wParam, lParam);
76}

差不多可以了,调试一下。结果,在 thunk``` 的第一行出错了。我原以为地址算错了神马的,尝试把 ```thunk.m_mov``` 改为 ```0x90909090```,再运行,还是出错。于是傻掉了……过了好一会儿才意识到,可能是因为 ```thunk``` 在数据段,无法被执行。可是,很久很久以前偶滴一个敬爱的老师在 TC 中鼓捣程序运行时改变自身代码时,貌似无此问题啊。。。然后查呀查,原来是 Windows 在的数据执行保护搞的鬼。于是,需要用 ```VirtualAlloc``` 来申请一段有执行权限的内存。WTL 里面也是这么做的,不过它似乎维护了一块较大的可执行内存区作为 ```thunk 内存池,我们这里从简。最后,整个流程终于跑通了。最终代码清单如下:

  1#include <Windows.h>
  2#include <assert.h>
  3#include <map> 
  4#include <tchar.h>
  5#pragma pack(push,1)
  6typedef struct _StdCallThunk
  7{
  8    DWORD   m_mov;
  9    DWORD   m_this;
 10    BYTE    m_jmp;
 11    DWORD   m_relproc;
 12
 13} StdCallThunk;
 14#pragma pack(pop)
 15
 16class Window
 17{
 18public:
 19    Window();
 20    ~Window();
 21
 22public:
 23    BOOL Create();
 24
 25protected:
 26    LRESULT WndProc(UINT message, WPARAM wParam, LPARAM lParam);
 27
 28protected:
 29    HWND          m_hWnd;
 30    StdCallThunk *m_pThunk;
 31
 32protected:
 33    static LRESULT CALLBACK TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
 34    static LRESULT CALLBACK StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
 35    static std::map<DWORD, Window *> m_sWindows;
 36};
 37
 38std::map<DWORD, Window *> Window::m_sWindows;
 39
 40Window::Window()
 41{
 42
 43}
 44
 45Window::~Window()
 46{
 47    VirtualFree(m_pThunk, sizeof(StdCallThunk), MEM_RELEASE);
 48}
 49
 50BOOL Window::Create()
 51{
 52    LPCTSTR lpszClassName = _T("ClassName");
 53    HINSTANCE hInstance = GetModuleHandle(NULL);
 54
 55    WNDCLASSEX wcex    = { sizeof(WNDCLASSEX) };
 56    wcex.lpfnWndProc   = TempWndProc;
 57    wcex.hInstance     = hInstance;
 58    wcex.lpszClassName = lpszClassName;
 59    wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
 60
 61    RegisterClassEx(&wcex);
 62
 63    DWORD dwThreadId = GetCurrentThreadId();
 64    m_sWindows.insert(std::make_pair(dwThreadId, this));
 65
 66    m_pThunk = (StdCallThunk *)VirtualAlloc(NULL, sizeof(StdCallThunk), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 67    m_pThunk->m_mov = 0x042444c7;
 68    m_pThunk->m_jmp = 0xe9;
 69
 70    m_hWnd = CreateWindow(lpszClassName, NULL, WS_OVERLAPPEDWINDOW,
 71        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
 72
 73    if (m_hWnd == NULL)
 74    {
 75        return FALSE;
 76    }
 77    
 78    ShowWindow(m_hWnd, SW_SHOW);
 79    UpdateWindow(m_hWnd);
 80
 81    return TRUE;
 82}
 83
 84LRESULT Window::WndProc(UINT message, WPARAM wParam, LPARAM lParam)
 85{
 86    switch (message)
 87    {
 88    case WM_LBUTTONUP:
 89        MessageBox(m_hWnd, _T("LButtonUp"), _T("Message"), MB_OK | MB_ICONINFORMATION);
 90        break;
 91    case WM_RBUTTONUP:
 92        MessageBox(m_hWnd, _T("RButtonUp"), _T("Message"), MB_OK | MB_ICONINFORMATION);
 93        break;
 94    case WM_DESTROY:
 95        PostQuitMessage(0);
 96        break;
 97    default:
 98        break;
 99    }
100
101    return DefWindowProc(m_hWnd, message, wParam, lParam);
102}
103
104LRESULT CALLBACK Window::TempWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
105{
106    std::map<DWORD, Window *>::iterator it = m_sWindows.find(GetCurrentThreadId());
107    assert(it != m_sWindows.end() && it->second != NULL);
108
109    Window *pThis = it->second;
110    m_sWindows.erase(it);
111
112    WNDPROC pWndProc = (WNDPROC)pThis->m_pThunk;
113
114    pThis->m_pThunk->m_this = (DWORD)pThis;
115    pThis->m_pThunk->m_relproc = (DWORD)&Window::StaticWndProc - ((DWORD)pThis->m_pThunk + sizeof(StdCallThunk));
116
117    pThis->m_hWnd = hWnd;
118    SetWindowLong(hWnd, GWL_WNDPROC, (LONG)pWndProc);
119
120    return pWndProc(hWnd, message, wParam, lParam);
121}
122
123LRESULT CALLBACK Window::StaticWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
124{
125    return ((Window *)hWnd)->WndProc(message, wParam, lParam);
126}
127
128int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
129{
130    Window wnd;
131    wnd.Create();
132
133    MSG msg;
134
135    while (GetMessage(&msg, NULL, 0, 0))
136    {
137        TranslateMessage(&msg);
138        DispatchMessage(&msg);
139    }
140
141    return (int)msg.wParam;
142}

刚才有一处,存 this``` 指针的时候,我很武断地把它与当前线程 ID 关联起来了,其实这正是 WTL 本身的做法。它用 ```CAtlWinModule::AddCreateWndData``` 存的 ```this```,最终会把当前线程 ID 和 ```this``` 作关联。我是这么理解的吧,同一线程不可能同时有两处在调用 ```CreateWindow```,所以这样取回来的 ```this 是可靠的。

好了,到此为止,边试验边记录的,不知道理解是否正确。欢迎指出不当之处,也欢迎提出相关的问题来考我,欢迎介绍有关此问题的新方法、新思路,等等,总之,请各位看官多指教哈。


首发:http://www.cppblog.com/Streamlet/archive/2010/10/24/131064.html



NoteIsSite/0.4